/******************************************************************
*
* CyberLink for Java
*
* Copyright (C) Satoshi Konno 2002-2003
*
* File: Service.java
*
* Revision;
*
* 11/28/02
*   - first revision.
* 04/12/02
*   - Holmes, Arran C <acholm@essex.ac.uk>
*   - Fixed SERVICE_ID constant instead of "serviceId".
* 06/17/03
*   - Added notifyAllStateVariables().
* 09/03/03
*   - Giordano Sassaroli <sassarol@cefriel.it>
*   - Problem : The device does not accepts request for services when control or subscription urls are absolute
*   - Error : device methods, when requests are received, search for services that have a controlUrl (or eventSubUrl) equal to the request URI
*             but request URI must be relative, so they cannot equal absolute urls
* 09/03/03
*   - Steven Yen
*   - description: to retrieve service information based on information in URLBase and SCPDURL
*   - problem: not able to retrieve service information when URLBase is missing and SCPDURL is relative
*   - fix: modify to retrieve host information from Header's Location (required) field and update the
*          BaseURL tag in the xml so subsequent information retrieval can be done (Steven Yen, 8.27.2003)
*   - note: 1. in the case that Header's Location field combine with SCPDURL is not able to retrieve proper 
*             information, updating BaseURL would not hurt, since exception will be thrown with or without update.
*           2. this problem was discovered when using PC running MS win XP with ICS enabled (gateway). 
*             It seems that  root device xml file does not have BaseURL and SCPDURL are all relative.
*           3. UPnP device architecture states that BaseURL is optional and SCPDURL may be relative as 
*             specified by UPnP vendor, so MS does not seem to violate the rule.
* 10/22/03
*   - Added setActionListener().
* 01/04/04
*   - Changed about new QueryListener interface.
* 01/06/04
*   - Moved the following methods to StateVariable class.
*     getQueryListener() 
*     setQueryListener() 
*     performQueryListener()
*   - Added new setQueryListener() to set a listner to all state variables.
* 07/02/04
*   - Added serviceSearchResponse().
*   - Deleted getLocationURL().
*   - Fixed announce() to set the root device URL to the LOCATION field.
* 07/31/04
*   - Changed notify() to remove the expired subscribers and not to remove the invalid response subscribers for NMPR.
*
******************************************************************/

package org.cybergarage.upnp;

import java.io.*;
import java.net.*;

import java.util.logging.Logger;

import org.cybergarage.http.*;
import org.cybergarage.xml.*;
import org.cybergarage.util.*;

import org.cybergarage.upnp.ssdp.*;
import org.cybergarage.upnp.xml.*;
import org.cybergarage.upnp.device.*;
import org.cybergarage.upnp.control.*;
import org.cybergarage.upnp.event.*;

public class Service
{
  private static Logger logger = Logger.getLogger("org.cybergarage.upnp");


  ////////////////////////////////////////////////
  //  Constants
  ////////////////////////////////////////////////
  
  public final static String ELEM_NAME = "service";


  ////////////////////////////////////////////////
  //  Member
  ////////////////////////////////////////////////

  private Node serviceNode;

  public Node getServiceNode()
  {
    return serviceNode;
  }

  ////////////////////////////////////////////////
  //  Constructor
  ////////////////////////////////////////////////

  public Service(Node node)
  {
    serviceNode = node;
  }

  ////////////////////////////////////////////////
  // Mutex
  ////////////////////////////////////////////////
  
  private Mutex mutex = new Mutex();
  
  public void lock()
  {
    mutex.lock();
  }
  
  public void unlock()
  {
    mutex.unlock();
  }
  
  ////////////////////////////////////////////////
  //  isServiceNode
  ////////////////////////////////////////////////

  public static boolean isServiceNode(Node node)
  {
    return Service.ELEM_NAME.equals(node.getName());
  }
  
  ////////////////////////////////////////////////
  //  Device/Root Node
  ////////////////////////////////////////////////

  private Node getDeviceNode()
  {
    Node node = getServiceNode().getParentNode();
    if (node == null)
      return null;
    return node.getParentNode();
  }

  private Node getRootNode()
  {
    return getServiceNode().getRootNode();
  }

  ////////////////////////////////////////////////
  //  Device
  ////////////////////////////////////////////////

  public Device getDevice()
  {
    return new Device(getRootNode(), getDeviceNode());
  }

  public Device getRootDevice()
  {
    return getDevice().getRootDevice();
  }

  ////////////////////////////////////////////////
  //  Description URL
  ////////////////////////////////////////////////

  public void setDescriptionURL(String value)
  {
    getServiceData().setDescriptionURL(value);
  }

  public String getDescriptionURL()
  {
    return getServiceData().getDescriptionURL();
  }

  ////////////////////////////////////////////////
  //  serviceType
  ////////////////////////////////////////////////

  private final static String SERVICE_TYPE = "serviceType";
  
  public void setServiceType(String value)
  {
    getServiceNode().setNode(SERVICE_TYPE, value);
  }

  public String getServiceType()
  {
    return getServiceNode().getNodeValue(SERVICE_TYPE);
  }

  ////////////////////////////////////////////////
  //  serviceID
  ////////////////////////////////////////////////

  private final static String SERVICE_ID = "serviceId";
  
  public void setServiceID(String value)
  {
    getServiceNode().setNode(SERVICE_ID, value);
  }

  public String getServiceID()
  {
    return getServiceNode().getNodeValue(SERVICE_ID);
  }

  ////////////////////////////////////////////////
  //  SCPDURL
  ////////////////////////////////////////////////

  private final static String SCPDURL = "SCPDURL";
  
  public void setSCPDURL(String value)
  {
    getServiceNode().setNode(SCPDURL, value);
  }

  public String getSCPDURL()
  {
    return getServiceNode().getNodeValue(SCPDURL);
  }

  ////////////////////////////////////////////////
  //  isURL
  ////////////////////////////////////////////////
  
  // Thanks for Giordano Sassaroli <sassarol@cefriel.it> (09/03/03)
  private boolean isURL(String referenceUrl, String url)
  {
    if (referenceUrl ==null || url == null)
      return false;
    boolean ret = url.equals(referenceUrl);
    if (ret == true)
      return true;
    String relativeRefUrl = HTTP.toRelativeURL(referenceUrl, false);
    ret = url.equals(relativeRefUrl);
    if (ret == true)
      return true;
    return false;
  }
  
  ////////////////////////////////////////////////
  //  controlURL
  ////////////////////////////////////////////////

  private final static String CONTROL_URL = "controlURL";
  
  public void setControlURL(String value)
  {
    getServiceNode().setNode(CONTROL_URL, value);
  }

  public String getControlURL()
  {
    return getServiceNode().getNodeValue(CONTROL_URL);
  }

  public boolean isControlURL(String url)
  {
    return isURL(getControlURL(), url);
  }

  ////////////////////////////////////////////////
  //  eventSubURL
  ////////////////////////////////////////////////

  private final static String EVENT_SUB_URL = "eventSubURL";
  
  public void setEventSubURL(String value)
  {
    getServiceNode().setNode(EVENT_SUB_URL, value);
  }

  public String getEventSubURL()
  {
    return getServiceNode().getNodeValue(EVENT_SUB_URL);
  }

  public boolean isEventSubURL(String url)
  {
    return isURL(getEventSubURL(), url);
  }
  
  ////////////////////////////////////////////////
  //  SCPD node
  ////////////////////////////////////////////////

  private Node getSCPDNode(String urlStr) throws MalformedURLException, ParserException
  {
    //logger.fine( " getSCPDNode(urlStr) Entered - urlStr: " + urlStr );
    
    URL url =  new URL(urlStr);
    Parser parser = UPnP.getXMLParser();
    return parser.parse(url);
  }

  private Node getSCPDNode(File file) throws ParserException
  {
    //logger.fine( " getSCPDNode(file) Entered file: " + file.toString() );

    Parser parser = UPnP.getXMLParser();
    return parser.parse(file);
  }

  private Node getSCPDNode()
  {
    Node serviceNode = getServiceNode();
    ServiceData data = getServiceData();
    Node scpdNode = data.getSCPDNode();
    if (scpdNode != null)
      return scpdNode;
    
    String scpdURLStr = getSCPDURL();

    logger.finest( " getSCPDNode loading service description - URL: " 
                 + scpdURLStr );

    Device rootDev = getRootDevice();
    
    String location = rootDev.getLocation();

    // OJN Mod - If location not set, we are always loading service file
    // from disk I think, so just do that
    if( location == null || location.length() <= 0)
    {
      // value from getDescriptionFilePath() doesn't have trailing '/'.
      // If scpdURLStr doesn't have a leading '/', add one
      String newScpdURLStr;
      if( scpdURLStr.startsWith("/") )
        newScpdURLStr = rootDev.getDescriptionFilePath() + scpdURLStr;
      else
        newScpdURLStr = rootDev.getDescriptionFilePath() + "/" + scpdURLStr;

      logger.finest( " Reading service description from local file: " +
                     newScpdURLStr );
      try {
        scpdNode = getSCPDNode(new File(newScpdURLStr));
      }
      catch (Exception e4) {
        Debug.warning(e4);
      }
    }
    else
    {
      try {
        scpdNode = getSCPDNode(scpdURLStr);
      }
      catch (Exception e1) {
        String urlBaseStr = rootDev.getURLBase();

        // Thanks for Steven Yen (2003/09/03)
        // Most often, URLBase will be NULL (Intel NMPR requires devices
        // don't define it). In this case, generate the base URL from
        // the description file URL
        if (urlBaseStr == null || urlBaseStr.length() <= 0)
        {
          location = rootDev.getLocation();
          //String location = rootDev.getLocation();
          String locationHost = HTTP.getHost(location);
          int locationPort = HTTP.getPort(location);
          urlBaseStr = HTTP.getRequestHostURL(locationHost, locationPort);
        }

        scpdURLStr = HTTP.toRelativeURL(scpdURLStr);

        // If URLBase has a trailing '/', trim it off since scpdURLStr
        // returned from above always has a leading '/' (TODO: Rethink this)
        if( urlBaseStr.endsWith("/") )
          urlBaseStr = urlBaseStr.substring(0,urlBaseStr.length()-1);

        String newScpdURLStr = urlBaseStr + scpdURLStr;

        //logger.warning( " getSCPDNode failed - trying service description - URL: " + newScpdURLStr );

        try {
          scpdNode = getSCPDNode(newScpdURLStr);
        }
        catch (Exception e2) {
          newScpdURLStr = HTTP.getAbsoluteURL(urlBaseStr, scpdURLStr);

          logger.warning( " getSCPDNode failed - trying service description - URL: "                      + newScpdURLStr );
          
          try {
            scpdNode = getSCPDNode(newScpdURLStr);
          }
          catch (Exception e3) {
            Debug.warning(e3);
          }
        }
      }
    }

    data.setSCPDNode(scpdNode);
    
    return scpdNode;
  }

  ////////////////////////////////////////////////
  //  actionList
  ////////////////////////////////////////////////

  public ActionList getActionList()
  {
    ActionList actionList = new ActionList();

    Node scdpNode = getSCPDNode();
    if (scdpNode == null)
      return actionList;  // Return empty list

    Node actionListNode = scdpNode.getNode(ActionList.ELEM_NAME);
    if (actionListNode == null)
      return actionList;  // Return empty list

    Node serviceNode = getServiceNode();
    int nNode = actionListNode.getNNodes();

    for (int n=0; n<nNode; n++) {
      Node node = actionListNode.getNode(n);
      if (Action.isActionNode(node) == false)
        continue;
      Action action = new Action(serviceNode, node);
      actionList.add(action);
    } 
    return actionList;
  }

  /**
   * Get an instance of the named action from the service, for use with
   * control points
   */ 
  public Action getAction(String actionName)
  {
    ActionList actionList = getActionList();
    int nActions = actionList.size();
    for (int n=0; n<nActions; n++) {
      Action action = actionList.getAction(n);
      String name = action.getName();
      if (name == null)
        continue;
      if (name.equals(actionName) == true)
        return action;
    }
    return null;
  }
  
  ////////////////////////////////////////////////
  //  serviceStateTable
  ////////////////////////////////////////////////

  /**
   * Add a state variable to the state variable table for this service
   * State variables normally added to node tree by reading device
   * description XML - this is a hack to help support devices who report
   * values for state variables that were not included in their device
   * description (OJN)
   */
  public void addStateVariable( String name, String dataType )
  {
    Node stateTableNode = getSCPDNode().getNode( ServiceStateTable.ELEM_NAME );
    if( stateTableNode == null )
      return;

    Node varNode = new Node( "stateVariable" );

    Node varNameNode = new Node( "name" );
    varNameNode.setValue( name );
    varNode.addNode( varNameNode );
    
    Node varTypeNode = new Node( "dataType" );
    varTypeNode.setValue( dataType );
    varNode.addNode( varTypeNode );

    stateTableNode.addNode( varNode );
  }
  
  /**
   * Get service state table. 
   *
   * @return  State variable table reference
   */  
  public ServiceStateTable getServiceStateTable()
  {
    ServiceStateTable stateTable = new ServiceStateTable();
    Node stateTableNode = getSCPDNode().getNode(ServiceStateTable.ELEM_NAME);
    if (stateTableNode == null)
      return stateTable;

    Node serviceNode = getServiceNode();
    int nNode = stateTableNode.getNNodes();
    for (int n=0; n<nNode; n++) {
      Node node = stateTableNode.getNode(n);
      if (StateVariable.isStateVariableNode(node) == false)
        continue;
      StateVariable serviceVar = new StateVariable(serviceNode, node);
      stateTable.add(serviceVar);
    } 
    return stateTable;
  }

  /**
   * Get service state variable by name. 
   *
   * @return  State variable reference, or null if no variable with matching
   *          name exists
   */  
  public StateVariable getStateVariable(String name)
  {
    ServiceStateTable stateTable = getServiceStateTable();

    int tableSize = stateTable.size();
    for (int n=0; n<tableSize; n++)
    {
      StateVariable var = stateTable.getStateVariable(n);
      String varName = var.getName();
      if (varName == null)
        continue;
      if (varName.equals(name) == true)
        return var;
    }
    return null;
  }
  
  public boolean hasStateVariable(String name)
  {
    return (getStateVariable(name) != null ) ? true : false;
  }

  ////////////////////////////////////////////////
  //  UserData
  ////////////////////////////////////////////////
  
  public boolean isService(String name)
  {
    if (name == null)
      return false;
    if (name.endsWith(getServiceType()) == true)
      return true;
    if (name.endsWith(getServiceID()) == true)
      return true;
    return false;
  }
   
  ////////////////////////////////////////////////
  //  UserData
  ////////////////////////////////////////////////

  private ServiceData getServiceData()
  {
    Node node = getServiceNode();
    ServiceData userData = (ServiceData)node.getUserData();
    if (userData == null) {
      userData = new ServiceData();
      node.setUserData(userData);
      userData.setNode(node);
    }
    return userData;
  }

  ////////////////////////////////////////////////
  //  Notify
  ////////////////////////////////////////////////

  private String getNotifyServiceTypeNT()
  {
    return getServiceType();
  }

  private String getNotifyServiceTypeUSN()
  {
    return getDevice().getUDN() + "::" + getServiceType();
  }
    
  public void announce(String bindAddr)
  {
    // uuid:device-UUID::urn:schemas-upnp-org:service:serviceType:version 
    Device rootDev = getRootDevice();
    String devLocation = rootDev.getLocationURL(bindAddr);
    String serviceNT = getNotifyServiceTypeNT();      
    String serviceUSN = getNotifyServiceTypeUSN();

    Device dev = getDevice();
    
    SSDPNotifyRequest ssdpReq = new SSDPNotifyRequest();
    ssdpReq.setServer(UPnP.getServerName());
    ssdpReq.setLeaseTime(dev.getLeaseTime());
    ssdpReq.setLocation(devLocation);
    ssdpReq.setNTS(NTS.ALIVE);
    ssdpReq.setNT(serviceNT);
    ssdpReq.setUSN(serviceUSN);

    SSDPNotifySocket ssdpSock = new SSDPNotifySocket(bindAddr);
    Device.notifyWait();
    ssdpSock.post(ssdpReq);
  }

  public void byebye(String bindAddr)
  {
    // uuid:device-UUID::urn:schemas-upnp-org:service:serviceType:version 
    
    String devNT = getNotifyServiceTypeNT();      
    String devUSN = getNotifyServiceTypeUSN();
    
    SSDPNotifyRequest ssdpReq = new SSDPNotifyRequest();
    ssdpReq.setNTS(NTS.BYEBYE);
    ssdpReq.setNT(devNT);
    ssdpReq.setUSN(devUSN);

    SSDPNotifySocket ssdpSock = new SSDPNotifySocket(bindAddr);
    Device.notifyWait();
    ssdpSock.post(ssdpReq);
  }

  public boolean serviceSearchResponse(SSDPPacket ssdpPacket)
  {
    String ssdpST = ssdpPacket.getST();

    if (ssdpST == null)
      return false;
      
    Device dev = getDevice();
      
    String serviceNT = getNotifyServiceTypeNT();      
    String serviceUSN = getNotifyServiceTypeUSN();
    
    if (ST.isAllDevice(ssdpST) == true) {
      dev.postSearchResponse(ssdpPacket, serviceNT, serviceUSN);
    }
    else if (ST.isURNService(ssdpST) == true) {
      String serviceType = getServiceType();
      if (ssdpST.equals(serviceType) == true)
        dev.postSearchResponse(ssdpPacket, serviceType, serviceUSN);
    }
    
    return true;
  }
  
  ////////////////////////////////////////////////
  // QueryListener
  ////////////////////////////////////////////////

  public void setQueryListener(QueryListener queryListener) 
  {
    ServiceStateTable stateTable = getServiceStateTable();
    int tableSize = stateTable.size();
    for (int n=0; n<tableSize; n++) {
      StateVariable var = stateTable.getStateVariable(n);
      var.setQueryListener(queryListener);
    }
  }
  
  ////////////////////////////////////////////////
  //  Subscription
  ////////////////////////////////////////////////

  private SubscriberList getSubscriberList() 
  {
    return getServiceData().getSubscriberList();
  }

  public void addSubscriber(Subscriber sub) 
  {
    logger.finest("adding subscriber " + sub.getSID() );
    getSubscriberList().add(sub);
  }

  public void removeSubscriber(Subscriber sub) 
  {
    logger.finest("removing subscriber " + sub.getSID() );
    getSubscriberList().remove(sub);
  }

  public Subscriber getSubscriber(String name) 
  {
    SubscriberList subList = getSubscriberList();
    int subListCnt = subList.size();
    for (int n=0; n<subListCnt; n++) {
      Subscriber sub = subList.getSubscriber(n);
      if (sub == null)
        continue;
      String sid = sub.getSID();
      if (sid == null)
        continue;
      if (sid.equals(name) == true)
        return sub;
    }
    return null;
  }

  private boolean notify(Subscriber sub, StateVariable stateVar)
  {
    String varName = stateVar.getName();
    String value = stateVar.getValue();
    
    logger.finest("notifying subscriber: " + sub.getSID() + 
                " var = " + varName + " value = " + value );

    String host = sub.getDeliveryHost();
    int port = sub.getDeliveryPort();
    String bindAddr = sub.getInterfaceAddress();
    
    NotifyRequest notifyReq = new NotifyRequest();
    notifyReq.setRequest(sub, varName, value);
    
    HTTPResponse res = notifyReq.post(host, port);
    if (res.getStatusCode() != HTTPStatus.OK)
      return false;
      
    sub.incrementNotifyCount();   
    
    return true;
  }

  /**
   *  Notify subscribers of a change in a state variable.
   *  Remove expired subscriptions and subscriptions that experience
   *  write errors (client no longer listening)
   */
  public void notify(StateVariable stateVar)
  {
    String varName = stateVar.getName();
    String value = stateVar.getValue();

    SubscriberList subList = getSubscriberList();
    int subListCnt = subList.size();

    logger.finest("notifying all subscribers (count=" + subListCnt + 
                  ") var = " + varName + " value = " + value );

    for (int n=0; n<subListCnt; n++)
    {
      Subscriber sub = subList.getSubscriber(n);
      if (sub.isExpired() == true) {
        removeSubscriber(sub);
        continue;
      }

      if (notify(sub, stateVar) == false) {
        removeSubscriber(sub);
      }
    }
  }

  /**
   * Send event notification for all evented state variables supported 
   * by service. This is done when a new subscriber connects to pass the
   * entire device state in one gulp.
   */
  public void notifyAllStateVariables()
  {
    logger.finest("\n NEW SUBSCRIBER - notifying all vars\n" );

    ServiceStateTable stateTable = getServiceStateTable();
    int tableSize = stateTable.size();
    for (int n=0; n<tableSize; n++)
    {
      StateVariable var = stateTable.getStateVariable(n);
      if (var.isSendEvents() == true)
      {
        // LastChange special case
        if( var.getName().equals("LastChange") )
        {
          updateLastChangeValue( var, true );  // indirectly invokes notify
        }
        else
        {
          notify(var);
        }
      }
    }
  }

  /**
   *  Update LastChange variable (AVTransport/RenderingControl special case)
   *  This version of the routine is for external use.
   */
  public void updateLastChangeStateVariable()
  {
    StateVariable lastChange = getStateVariable("LastChange");
    if( lastChange == null )
      return;
    
    updateLastChangeValue( lastChange, false );
  }
  
  /**
   *  Set the value of the special case 'LastChange' variable used to 
   *  consolidate state info into a single state variable.
   *
   *  Iterate over all non-evented state variables in the service and store
   *  the current value in the LastChange using the specified
   *  XML syntax. Only those variables that have changed since the
   *  last 'LastChange' event are included, EXCEPT if this is the 
   *  first event for a given subscriber, in which case all variables
   *  are included, regardless of their 'dirty' status
   *
   *  Sample of LastChange value XML:
   *
   *  <Event>
   *    <InstanceID val="0">
   *      <Volume val="76" />
   *      <Mute val="0" />
   *    </InstanceID>
   *  </Event>
   *
   *
   * @param    lastChangeStateVar
   * @param    firstEvent
   * @return   Count of variables sent in LastChange event
   *
   */
  public int updateLastChangeValue( StateVariable lastChangeStateVar,
                                    boolean firstEvent )
  {
    ServiceStateTable stateTable = getServiceStateTable();

    StringBuffer buf = null;

    //System.out.println("updateLastChangeValue: Entered\n");
    
    int tableSize = stateTable.size();
    int outCount = 0;

    for (int n=0; n<tableSize; n++)
    {
      StateVariable var = stateTable.getStateVariable(n);
      String varName = var.getName();
      
      if ( (var.isSendEvents() == false) && (!varName.startsWith("A_ARG")) &&
           (var.isDirty() || firstEvent) )
      {
        if( outCount == 0 )
        {
          buf = new StringBuffer();
          buf.append("<Event>\n");
          buf.append("<InstanceID val=\"0\">\n" );
        }
        
        // Need to escape any XML characters within the value
        buf.append("<" + var.getName() + " val=\"" +
                   XML.escapeXMLChars( var.getValue() ) + 
                   "\"/>\n" );

        var.clearDirty();
        outCount++;
      }
    }

    if( outCount == 0 )
      return outCount;

    buf.append("</InstanceID>\n");
    buf.append("</Event>");

    lastChangeStateVar.setDirty();
    lastChangeStateVar.setValue( buf.toString() );

    //System.out.println("updateLastChangeValue: Leaving \n[" + buf.toString() + "]" );

    return outCount;
  }

  ////////////////////////////////////////////////
  // SID
  ////////////////////////////////////////////////

  public String getSID() 
  {
    return getServiceData().getSID();
  }

  public void setSID(String id) 
  {
    getServiceData().setSID(id);
  }

  public void clearSID()
  {
    setSID("");
    setTimeout( ServiceData.SUBSCRIPTION_NOMINAL_TIMEOUT ); // Reset
  }
  
  public boolean hasSID()
  {
    return StringUtil.hasData(getSID());
  }   

  public boolean isSubscribed()
  {
    return hasSID();
  }
  
  ////////////////////////////////////////////////
  // Timeout
  ////////////////////////////////////////////////

  public long getTimeout() 
  {
    return getServiceData().getTimeout();
  }

  public void setTimeout(long value) 
  {
    getServiceData().setTimeout(value);
  }

  ////////////////////////////////////////////////
  // IsDeviceInstance
  ////////////////////////////////////////////////

	public void setIsDeviceInstance( boolean value ) 
	{
    getServiceData().setIsDeviceInstance(value);
	}

	public boolean getIsDeviceInstance()
	{
    return getServiceData().getIsDeviceInstance();
	}


  ////////////////////////////////////////////////
  // AcionListener
  ////////////////////////////////////////////////
  
  public void setActionListener(ActionListener listener)
  {
    ActionList actionList = getActionList();
    int nActions = actionList.size();
    for (int n=0; n<nActions; n++) {
      Action action = actionList.getAction(n);
      action.setActionListener(listener);
    }
  }

  ////////////////////////////////////////////////
  //  Event Listener 
  //
  //  OJN mod to have event listeners be installed on
  //  a per-service basis for convenience.
  // 
  ////////////////////////////////////////////////

  public void addEventListener(EventListener listener)
  {
    getServiceData().addEventListener( listener );
    logger.finest("(Service) Added service event listener " );
  }

  public void removeEventListener(EventListener listener)
  {
    getServiceData().removeEventListener( listener );
  }   

  public void performEventListener(String uuid, long seq,
                                   String name, String value)
  {
    logger.finest("(Service) Performing event listener");
    getServiceData().performEventListener( uuid, seq, name, value );
  }

}
